package org.example.travel.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.travel.dto.ScenicActionDTO;
import org.example.travel.dto.ScenicSpotSearchDTO;
import org.example.travel.entity.ScenicLog;
import org.example.travel.entity.ScenicSpot;
import org.example.travel.mapper.ScenicLogMapper;
import org.example.travel.mapper.ScenicSpotMapper;
import org.example.travel.service.ScenicSpotService;
import org.example.travel.common.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
@RequiredArgsConstructor
public class ScenicSpotServiceImpl implements ScenicSpotService {

    private final ScenicSpotMapper scenicSpotMapper;
    private final ScenicLogMapper scenicLogMapper;
    private final StringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public IPage<ScenicSpot> search(ScenicSpotSearchDTO dto) {
        boolean useCache = !StringUtils.hasText(dto.getKeyword()) && !StringUtils.hasText(dto.getLocation());

        if (useCache) {
            String json = redisTemplate.opsForValue().get("hot:scenic:top");
            if (StringUtils.hasText(json)) {
                try {
                    List<ScenicSpot> cached = objectMapper.readValue(json, new TypeReference<>() {});
                    int page = dto.getPage() != null ? dto.getPage() : 1;
                    int size = dto.getSize() != null ? dto.getSize() : 10;
                    int from = (page - 1) * size;
                    int to = Math.min(from + size, cached.size());
                    List<ScenicSpot> subList = from >= cached.size() ? Collections.emptyList() : cached.subList(from, to);
                    Page<ScenicSpot> result = new Page<>(page, size, cached.size());
                    result.setRecords(subList);
                    return result;
                } catch (JsonProcessingException e) {
                    throw new RuntimeException("缓存解析失败", e);
                }
            }
        }

        QueryWrapper<ScenicSpot> query = new QueryWrapper<>();
        if (StringUtils.hasText(dto.getKeyword())) {
            query.like("name", dto.getKeyword());
        }
        if (StringUtils.hasText(dto.getLocation())) {
            query.like("location", dto.getLocation());
        }
        query.orderByDesc("heat_score");
        Page<ScenicSpot> page = new Page<>(dto.getPage(), dto.getSize());
        return scenicSpotMapper.selectPage(page, query);
    }

    @Override
    public void logAction(ScenicActionDTO dto) {
        Long userId = UserHolder.getUserId();
        ScenicLog log = new ScenicLog();
        log.setUserId(userId);
        log.setSpotId(dto.getSpotId());
        log.setActionType(dto.getActionType());
        log.setCreateTime(LocalDateTime.now());
        try {
            String json = objectMapper.writeValueAsString(log);
            redisTemplate.opsForList().rightPush("scenic:log", json);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("记录用户行为失败", e);
        }
    }

    @Override
    public void flushScenicLogs() {
        List<String> logs = redisTemplate.opsForList().range("scenic:log", 0, -1);
        if (logs == null || logs.isEmpty()) return;

        List<ScenicLog> logList = logs.stream().map(json -> {
            try {
                return objectMapper.readValue(json, ScenicLog.class);
            } catch (JsonProcessingException e) {
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());

        logList.forEach(scenicLogMapper::insert);
        redisTemplate.delete("scenic:log");
    }

    @Override
    public void updateHeatScoresAndCache() {
        LocalDateTime cutoff = LocalDateTime.now().minusDays(1);
        QueryWrapper<ScenicLog> query = new QueryWrapper<>();
        query.ge("create_time", cutoff);
        List<ScenicLog> logs = scenicLogMapper.selectList(query);

        Map<Long, List<ScenicLog>> logMap = logs.stream().collect(Collectors.groupingBy(ScenicLog::getSpotId));
        List<Long> spotIds = new ArrayList<>(logMap.keySet());
        List<ScenicSpot> spotList = scenicSpotMapper.selectBatchIds(spotIds);

        for (ScenicSpot spot : spotList) {
            List<ScenicLog> spotLogs = logMap.getOrDefault(spot.getSpotId(), Collections.emptyList());
            long viewInc = spotLogs.stream().filter(l -> l.getActionType() == 0).count();
            long collectInc = spotLogs.stream().filter(l -> l.getActionType() == 1).count();
            spot.setView(spot.getView() + viewInc);
            spot.setCollect(spot.getCollect() + collectInc);
            spot.setHeatScore(spot.getView() * 0.4 + spot.getCollect() * 0.6);
            scenicSpotMapper.updateById(spot);
        }

        List<ScenicSpot> allSpots = scenicSpotMapper.selectList(null);
        List<ScenicSpot> top20 = allSpots.stream()
                .sorted(Comparator.comparingDouble(ScenicSpot::getHeatScore).reversed())
                .limit(20).collect(Collectors.toList());

        try {
            String json = objectMapper.writeValueAsString(top20);
            int ttl = 86400 + new Random().nextInt(600);
            redisTemplate.opsForValue().set("hot:scenic:top", json, ttl, TimeUnit.SECONDS);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("缓存写入失败", e);
        }
    }
}